// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Collections.Generic;
using System.IO;

namespace BlueOnionSoftware.Bloget
{
    internal class NonContiguousMemoryStream : Stream
    {
        private const int BufferStoreSize = 64*1024;
        private List<byte[]> _buffersStore = new List<byte[]>();
        private long _position;
        private long _length;
        private bool _disposed;

        public NonContiguousMemoryStream()
        {
        }

        public NonContiguousMemoryStream(byte[] buffer)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException(typeof (NonContiguousMemoryStream).ToString());
            }

            Write(buffer, 0, buffer.Length);
        }

        public override void WriteByte(byte value)
        {
            Write(new[] {value}, 0, 1);
        }

        public void WriteTo(Stream stream)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
            }

            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            var countMinusOne = _buffersStore.Count - 1;

            if (countMinusOne >= 0)
            {
                for (var i = 0; i < countMinusOne; i++)
                {
                    stream.Write(_buffersStore[i], 0, BufferStoreSize);
                }

                var remaining = (int) (_length%BufferStoreSize);

                if (remaining == 0)
                {
                    remaining = BufferStoreSize;
                }

                stream.Write(_buffersStore[countMinusOne], 0, remaining);
            }
        }

        public override bool CanRead
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                return true;
            }
        }

        public override bool CanSeek
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                return true;
            }
        }

        public override bool CanWrite
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                return true;
            }
        }

        public override void Flush()
        {
        }

        public override long Length
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                return _length;
            }
        }

        public override long Position
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                return _position;
            }

            set
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
                }

                if (value < 0 || (_length > 0 && value >= _length))
                {
                    throw new InvalidDataException("Position out of range");
                }

                _position = value;
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
            }

            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }

            if (offset < 0 || offset >= buffer.Length)
            {
                throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset < buffer.Length");
            }

            if (count < 0 || count > buffer.Length)
            {
                throw new ArgumentOutOfRangeException("count", count, "0 <= count < buffer.Length");
            }

            if ((offset + count) > buffer.Length)
            {
                throw new ArgumentOutOfRangeException("offset", (offset + count), "(offset + count) <= buffer.Length");
            }

            var end = offset + count;
            var remaining = _length - _position;

            if (remaining < end)
            {
                end = (int) remaining;
            }

            var startingOffset = offset;
            var buffersStoreIndex = (int) (_position/BufferStoreSize);
            var bufferStoreOffset = (int) (_position%BufferStoreSize);
            var bufferStore = _buffersStore[buffersStoreIndex];

            while ((offset < end) && (_position < _length))
            {
                var run = Math.Min(end - offset, BufferStoreSize - bufferStoreOffset);
                Buffer.BlockCopy(bufferStore, bufferStoreOffset, buffer, offset, run);
                offset += run;
                bufferStoreOffset += run;
                _position += run;

                if (bufferStoreOffset == BufferStoreSize && _position < _length)
                {
                    buffersStoreIndex += 1;
                    bufferStore = _buffersStore[buffersStoreIndex];
                    bufferStoreOffset = 0;
                }
            }

            return (offset - startingOffset);
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
            }

            if (offset > int.MaxValue)
            {
                throw new ArgumentOutOfRangeException("offset", "offset > Int32.MaxValue");
            }

            var newPosition = _position;

            switch (origin)
            {
                case SeekOrigin.Begin:
                    if (offset < 0)
                    {
                        throw new IOException("Seek before Begin");
                    }

                    newPosition = offset;
                    break;

                case SeekOrigin.Current:
                    if ((_position + offset) < 0)
                    {
                        throw new IOException("Seek before Begin");
                    }

                    newPosition += offset;
                    break;

                case SeekOrigin.End:
                    if ((_length + offset) < 0)
                    {
                        throw new IOException("Seek before Begin");
                    }

                    newPosition = _length + offset;
                    break;

                default:
                    throw new ArgumentException("Invalid SeekOrgin", "origin");
            }

            _position = newPosition;
            return _position;
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException("The method is not implemented.");
        }

        public override sealed void Write(byte[] buffer, int offset, int count)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(typeof (NonContiguousMemoryStream).ToString());
            }

            if (buffer == null)
            {
                throw new ArgumentNullException("buffer");
            }

            if (offset < 0 || offset >= buffer.Length)
            {
                throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset < buffer.Length");
            }

            if (count < 0 || count > buffer.Length)
            {
                throw new ArgumentOutOfRangeException("count", count, "0 <= count < buffer.Length");
            }

            if ((offset + count) > buffer.Length)
            {
                throw new ArgumentOutOfRangeException("offset", (offset + count), "(offset + count) <= buffer.Length");
            }

            while ((_position + count) > (_buffersStore.Count*BufferStoreSize))
            {
                _buffersStore.Add(new byte[BufferStoreSize]);
            }

            var end = offset + count;
            var buffersStoreIndex = (int) (_position/BufferStoreSize);
            var bufferStoreOffset = (int) (_position%BufferStoreSize);
            var bufferStore = _buffersStore[buffersStoreIndex];

            while (offset < end)
            {
                var run = Math.Min(end - offset, BufferStoreSize - bufferStoreOffset);
                Buffer.BlockCopy(buffer, offset, bufferStore, bufferStoreOffset, run);
                offset += run;
                bufferStoreOffset += run;

                if (bufferStoreOffset == BufferStoreSize && offset < end)
                {
                    buffersStoreIndex += 1;
                    bufferStore = _buffersStore[buffersStoreIndex];
                    bufferStoreOffset = 0;
                }
            }

            _position += count;

            if (_position > _length)
            {
                _length = _position;
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            try
            {
                if (disposing)
                {
                    _disposed = true;
                    _length = 0;
                    _position = 0;
                    _buffersStore.Clear();
                    _buffersStore = null;
                }
            }

            finally
            {
                base.Dispose(disposing);
            }
        }
    }
}